home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / hity wydania / Ubuntu 9.10 PL / karmelkowy-koliberek-desktop-9.10-i386-PL.iso / casper / filesystem.squashfs / usr / share / pyshared / papyon / conversation.py < prev    next >
Text File  |  2009-10-08  |  16KB  |  448 lines

  1. # -*- coding: utf-8 -*-
  2. #
  3. # papyon - a python client library for Msn
  4. #
  5. # Copyright (C) 2005-2007 Ali Sabil <ali.sabil@gmail.com>
  6. # Copyright (C) 2007 Johann Prieur <johann.prieur@gmail.com>
  7. #
  8. # This program is free software; you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation; either version 2 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with this program; if not, write to the Free Software
  20. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  21.  
  22. """Conversation
  23.  
  24. This module contains the classes needed to have a conversation with a
  25. contact."""
  26.  
  27. import msnp
  28. import p2p
  29. from switchboard_manager import SwitchboardClient
  30. from papyon.event import EventsDispatcher
  31. from papyon.profile import NetworkID
  32.  
  33. import logging
  34. import gobject
  35. from urllib import quote, unquote
  36.  
  37. __all__ = ['Conversation', 'ConversationInterface', 'ConversationMessage', 'TextFormat']
  38.  
  39. logger = logging.getLogger('papyon.conversation')
  40.  
  41.  
  42. def Conversation(client, contacts):
  43.     """Factory function used to create the appropriate conversation with the
  44.     given contacts.
  45.  
  46.     This is the method you need to use to start a conversation with both MSN
  47.     users and Yahoo! users.
  48.         @attention: you can only talk to one Yahoo! contact at a time, and you
  49.         cannot have multi-user conversations with both MSN and Yahoo! contacts.
  50.  
  51.         @param contacts: The list of contacts to invite into the conversation
  52.         @type contacts: [L{Contact<papyon.profile.Contact>}, ...]
  53.  
  54.         @returns: a Conversation object implementing L{ConversationInterface<papyon.conversation.ConversationInterface>}
  55.         @rtype: L{ConversationInterface<papyon.conversation.ConversationInterface>}
  56.     """
  57.     msn_contacts = set([contact for contact in contacts \
  58.             if contact.network_id == NetworkID.MSN])
  59.     external_contacts = set(contacts) - msn_contacts
  60.  
  61.     if len(external_contacts) == 0:
  62.         return SwitchboardConversation(client, contacts)
  63.     elif len(msn_contacts) != 0:
  64.         raise NotImplementedError("The protocol doesn't allow mixing " \
  65.                 "contacts from different networks in a single conversation")
  66.     elif len(external_contacts) > 1:
  67.         raise NotImplementedError("The protocol doesn't allow having " \
  68.                 "more than one external contact in a conversation")
  69.     elif len(external_contacts) == 1:
  70.         return ExternalNetworkConversation(client, contacts)
  71.  
  72.  
  73. class ConversationInterface(object):
  74.     """Interface implemented by all the Conversation objects, a Conversation
  75.     object allows the user to communicate with one or more peers"""
  76.  
  77.     def send_text_message(self, message):
  78.         """Send a message to all persons in this conversation.
  79.  
  80.             @param message: the message to send to the users on this conversation
  81.             @type message: L{Contact<papyon.profile.Contact>}"""
  82.         raise NotImplementedError
  83.  
  84.     def send_nudge(self):
  85.         """Sends a nudge to the contacts on this conversation."""
  86.         raise NotImplementedError
  87.  
  88.     def send_typing_notification(self):
  89.         """Sends an user typing notification to the contacts on this
  90.         conversation."""
  91.         raise NotImplementedError
  92.  
  93.     def invite_user(self, contact):
  94.         """Request a contact to join in the conversation.
  95.  
  96.             @param contact: the contact to invite.
  97.             @type contact: L{Contact<papyon.profile.Contact>}"""
  98.         raise NotImplementedError
  99.  
  100.     def leave(self):
  101.         """Leave the conversation."""
  102.         raise NotImplementedError
  103.  
  104.  
  105. class ConversationMessage(object):
  106.     """A Conversation message sent or received
  107.  
  108.         @ivar display_name: the display name to show for the sender of this message
  109.         @type display_name: utf-8 encoded string
  110.  
  111.         @ivar content: the content of the message
  112.         @type content: utf-8 encoded string
  113.  
  114.         @ivar formatting: the formatting for this message
  115.         @type formatting: L{TextFormat<papyon.conversation.TextFormat>}
  116.  
  117.         @ivar msn_objects: a dictionary mapping smileys
  118.             to an L{MSNObject<papyon.p2p.MSNObject>}
  119.         @type msn_objects: {smiley: string => L{MSNObject<papyon.p2p.MSNObject>}}
  120.     """
  121.     def __init__(self, content, formatting=None, msn_objects={}):
  122.         """Initializer
  123.  
  124.             @param content: the content of the message
  125.             @type content: utf-8 encoded string
  126.  
  127.             @param formatting: the formatting for this message
  128.             @type formatting: L{TextFormat<papyon.conversation.TextFormat>}
  129.  
  130.             @param msn_objects: a dictionary mapping smileys
  131.                 to an L{MSNObject<papyon.p2p.MSNObject>}
  132.             @type msn_objects: {smiley: string => L{MSNObject<papyon.p2p.MSNObject>}}"""
  133.         self.display_name = None
  134.         self.content = content
  135.         self.formatting = formatting
  136.         self.msn_objects = msn_objects
  137.  
  138. class TextFormat(object):
  139.  
  140.     DEFAULT_FONT = 'MS Sans Serif'
  141.  
  142.     # effects
  143.     NO_EFFECT = 0
  144.     BOLD = 1
  145.     ITALIC = 2
  146.     UNDERLINE = 4
  147.     STRIKETHROUGH = 8
  148.  
  149.     # charset
  150.     ANSI_CHARSET = '0'
  151.     DEFAULT_CHARSET = '1'
  152.     SYMBOL_CHARSET = '2'
  153.     MAC_CHARSETLT = '4d'
  154.     SHIFTJIS_CHARSET = '80'
  155.     HANGEUL_CHARSET = '81'
  156.     JOHAB_CHARSET = '82'
  157.     GB2312_CHARSET = '86'
  158.     CHINESEBIG5_CHARSET = '88'
  159.     GREEK_CHARSET = 'a1'
  160.     TURKISH_CHARSET = 'a2'
  161.     VIETNAMESE_CHARSET = 'a3'
  162.     HEBREW_CHARSET = 'b1'
  163.     ARABIC_CHARSET = 'b2'
  164.     BALTIC_CHARSET = 'ba'
  165.     RUSSIAN_CHARSET_DEFAULT = 'cc'
  166.     THAI_CHARSET = 'de'
  167.     EASTEUROPE_CHARSET = 'ee'
  168.     OEM_DEFAULT = 'ff'
  169.  
  170.     # family
  171.     FF_DONTCARE = 0
  172.     FF_ROMAN = 1
  173.     FF_SWISS = 2
  174.     FF_MODERN = 3
  175.     FF_SCRIPT = 4
  176.     FF_DECORATIVE = 5
  177.  
  178.     # pitch
  179.     DEFAULT_PITCH = 0
  180.     FIXED_PITCH = 1
  181.     VARIABLE_PITCH = 2
  182.  
  183.     @staticmethod
  184.     def parse(format):
  185.         text_format = TextFormat()
  186.         text_format.__parse(format)
  187.         return text_format
  188.  
  189.     @property
  190.     def font(self):
  191.         return self._font
  192.  
  193.     @property
  194.     def style(self):
  195.         return self._style
  196.  
  197.     @property
  198.     def color(self):
  199.         return self._color
  200.  
  201.     @property
  202.     def right_alignment(self):
  203.         return self._right_alignment
  204.  
  205.     @property
  206.     def charset(self):
  207.         return self._charset
  208.  
  209.     @property
  210.     def pitch(self):
  211.         return self._pitch
  212.  
  213.     @property
  214.     def family(self):
  215.         return self._family
  216.  
  217.     def __init__(self, font=DEFAULT_FONT, style=NO_EFFECT, color='0',
  218.                  charset=DEFAULT_CHARSET, family=FF_DONTCARE,
  219.                  pitch=DEFAULT_PITCH, right_alignment=False):
  220.         self._font = font
  221.         self._style = style
  222.         self._color = color
  223.         self._charset = charset
  224.         self._pitch = pitch
  225.         self._family = family
  226.         self._right_alignment = right_alignment
  227.  
  228.     def __parse(self, format):
  229.         for property in format.split(';'):
  230.             key, value =  [p.strip(' \t|').upper() \
  231.                     for p in property.split('=', 1)]
  232.             if key == 'FN':
  233.                 # Font
  234.                 self._font = unquote(value)
  235.             elif key == 'EF':
  236.                 # Effects
  237.                 if 'B' in value: self._style |= TextFormat.BOLD
  238.                 if 'I' in value: self._style |= TextFormat.ITALIC
  239.                 if 'U' in value: self._style |= TextFormat.UNDERLINE
  240.                 if 'S' in value: self._style |= TextFormat.STRIKETHROUGH
  241.             elif key == 'CO':
  242.                 # Color
  243.                 value = value.zfill(6)
  244.                 self._color = ''.join((value[4:6], value[2:4], value[0:2]))
  245.             elif key == 'CS':
  246.                 # Charset
  247.                 self._charset = value
  248.             elif key == 'PF':
  249.                 # Family and pitch
  250.                 value = value.zfill(2)
  251.                 self._family = int(value[0])
  252.                 self._pitch = int(value[1])
  253.             elif key == 'RL':
  254.                 # Right alignment
  255.                 if value == '1': self._right_alignement = True
  256.  
  257.     def __str__(self):
  258.         style = ''
  259.         if self._style & TextFormat.BOLD == TextFormat.BOLD:
  260.             style += 'B'
  261.         if self._style & TextFormat.ITALIC == TextFormat.ITALIC:
  262.             style += 'I'
  263.         if self._style & TextFormat.UNDERLINE == TextFormat.UNDERLINE:
  264.             style += 'U'
  265.         if self._style & TextFormat.STRIKETHROUGH == TextFormat.STRIKETHROUGH:
  266.             style += 'S'
  267.  
  268.         color = '%s%s%s' % (self._color[4:6], self._color[2:4], self._color[0:2])
  269.  
  270.         format = 'FN=%s; EF=%s; CO=%s; CS=%s; PF=%d%d'  % (quote(self._font),
  271.                                                            style, color,
  272.                                                            self._charset,
  273.                                                            self._family,
  274.                                                            self._pitch)
  275.         if self._right_alignment: format += '; RL=1'
  276.  
  277.         return format
  278.  
  279.     def __repr__(self):
  280.         return __str__(self)
  281.  
  282.  
  283. class AbstractConversation(ConversationInterface, EventsDispatcher):
  284.     def __init__(self, client):
  285.         self._client = client
  286.         ConversationInterface.__init__(self)
  287.         EventsDispatcher.__init__(self)
  288.  
  289.         self.__last_received_msn_objects = {}
  290.  
  291.     def send_text_message(self, message):
  292.         if len(message.msn_objects) > 0:
  293.             body = []
  294.             for alias, msn_object in message.msn_objects.iteritems():
  295.                 self._client._msn_object_store.publish(msn_object)
  296.                 body.append(alias.encode("utf-8"))
  297.                 body.append(str(msn_object))
  298.                 # FIXME : we need to distinguish animemoticon and emoticons
  299.                 # and send the related msn objects in separated messages
  300.             self._send_message(("text/x-mms-animemoticon",), '\t'.join(body))
  301.  
  302.         content_type = ("text/plain","utf-8")
  303.         body = message.content.encode("utf-8")
  304.         ack = msnp.MessageAcknowledgement.HALF
  305.         headers = {}
  306.         if message.formatting is not None:
  307.             headers["X-MMS-IM-Format"] = str(message.formatting)
  308.  
  309.         self._send_message(content_type, body, headers, ack)
  310.  
  311.     def send_nudge(self):
  312.         content_type = "text/x-msnmsgr-datacast"
  313.         body = "ID: 1\r\n\r\n".encode('UTF-8') #FIXME: we need to figure out the datacast objects :D
  314.         ack = msnp.MessageAcknowledgement.NONE
  315.         self._send_message(content_type, body, ack=ack)
  316.  
  317.     def send_typing_notification(self):
  318.         content_type = "text/x-msmsgscontrol"
  319.         body = "\r\n\r\n".encode('UTF-8')
  320.         headers = { "TypingUser" : self._client.profile.account.encode('UTF_8') }
  321.         ack = msnp.MessageAcknowledgement.NONE
  322.         self._send_message(content_type, body, headers, ack)
  323.  
  324.     def invite_user(self, contact):
  325.         raise NotImplementedError
  326.  
  327.     def leave(self):
  328.         raise NotImplementedError
  329.  
  330.     def _send_message(self, content_type, body, headers={},
  331.             ack=msnp.MessageAcknowledgement.HALF):
  332.         raise NotImplementedError
  333.  
  334.     def _on_contact_joined(self, contact):
  335.         self._dispatch("on_conversation_user_joined", contact)
  336.  
  337.     def _on_contact_left(self, contact):
  338.         self._dispatch("on_conversation_user_left", contact)
  339.  
  340.     def _on_message_received(self, message):
  341.         sender = message.sender
  342.         message_type = message.content_type[0]
  343.         message_encoding = message.content_type[1]
  344.         try:
  345.             message_formatting = message.get_header('X-MMS-IM-Format')
  346.             if not message_formatting:
  347.                 message_formatting = '='
  348.         except KeyError:
  349.             message_formatting = '='
  350.  
  351.         if message_type == 'text/plain':
  352.             msg = ConversationMessage(unicode(message.body, message_encoding),
  353.                     TextFormat.parse(message_formatting),
  354.                     self.__last_received_msn_objects)
  355.             try:
  356.                 display_name = message.get_header('P4-Context')
  357.             except KeyError:
  358.                 display_name = sender.display_name
  359.             msg.display_name = display_name
  360.             self._dispatch("on_conversation_message_received", sender, msg)
  361.             self.__last_received_msn_objects = {}
  362.         elif message_type == 'text/x-msmsgscontrol':
  363.             self._dispatch("on_conversation_user_typing", sender)
  364.         elif message_type in ['text/x-mms-emoticon',
  365.                               'text/x-mms-animemoticon']:
  366.             msn_objects = {}
  367.             parts = message.body.split('\t')
  368.             logger.debug(parts)
  369.             for i in [i for i in range(len(parts)) if not i % 2]:
  370.                 if parts[i] == '': break
  371.                 msn_objects[parts[i]] = p2p.MSNObject.parse(self._client,
  372.                         parts[i+1])
  373.             self.__last_received_msn_objects = msn_objects
  374.         elif message_type == 'text/x-msnmsgr-datacast' and \
  375.                 message.body.strip() == "ID: 1":
  376.             self._dispatch("on_conversation_nudge_received", sender)
  377.  
  378.     def _on_message_sent(self, message):
  379.         pass
  380.  
  381.     def _on_error(self, error_type, error):
  382.         self._dispatch("on_conversation_error", error_type, error)
  383.  
  384.  
  385. class ExternalNetworkConversation(AbstractConversation):
  386.     def __init__(self, client, contacts):
  387.         AbstractConversation.__init__(self, client)
  388.         self.participants = set(contacts)
  389.         client._register_external_conversation(self)
  390.         gobject.idle_add(self._open)
  391.  
  392.     def _open(self):
  393.         for contact in self.participants:
  394.             self._on_contact_joined(contact)
  395.         return False
  396.  
  397.     def invite_user(self, contact):
  398.         raise NotImplementedError("The protocol doesn't allow multiuser " \
  399.                 "conversations for external contacts")
  400.  
  401.     def leave(self):
  402.         self._client._unregister_external_conversation(self)
  403.  
  404.     def _send_message(self, content_type, body, headers={},
  405.             ack=msnp.MessageAcknowledgement.HALF):
  406.         if content_type[0]  in ['text/x-mms-emoticon',
  407.                                 'text/x-mms-animemoticon']:
  408.             return
  409.         message = msnp.Message(self._client.profile)
  410.         for key, value in headers.iteritems():
  411.             message.add_header(key, value)
  412.         message.content_type = content_type
  413.         message.body = body
  414.         for contact in self.participants:
  415.             self._client._protocol.\
  416.                     send_unmanaged_message(contact, message)
  417.  
  418.  
  419. class SwitchboardConversation(AbstractConversation, SwitchboardClient):
  420.     def __init__(self, client, contacts):
  421.         SwitchboardClient.__init__(self, client, contacts, priority=0)
  422.         AbstractConversation.__init__(self, client)
  423.  
  424.     @staticmethod
  425.     def _can_handle_message(message, switchboard_client=None):
  426.         content_type = message.content_type[0]
  427.         if switchboard_client is None:
  428.             return content_type in ('text/plain', 'text/x-msnmsgr-datacast')
  429.         # FIXME : we need to not filter those 'text/x-mms-emoticon', 'text/x-mms-animemoticon'
  430.         return content_type in ('text/plain', 'text/x-msmsgscontrol',
  431.                 'text/x-msnmsgr-datacast', 'text/x-mms-emoticon',
  432.                 'text/x-mms-animemoticon')
  433.  
  434.     def invite_user(self, contact):
  435.         """Request a contact to join in the conversation.
  436.  
  437.             @param contact: the contact to invite.
  438.             @type contact: L{profile.Contact}"""
  439.         SwitchboardClient._invite_user(self, contact)
  440.  
  441.     def leave(self):
  442.         """Leave the conversation."""
  443.         SwitchboardClient._leave(self)
  444.  
  445.     def _send_message(self, content_type, body, headers={},
  446.             ack=msnp.MessageAcknowledgement.HALF):
  447.         SwitchboardClient._send_message(self, content_type, body, headers, ack)
  448.